AI 生成应用
本节重点
经过前面几期的基础搭建,我们的项目代码已经基本成型。本节我们将学习如何使用 LangChain4j 框架接入 AI 大模型,让 AI 根据用户的简单描述,自动生成完整的网页应用。
本节主要内容:
- 设计 AI 应用生成方案
- LangChain4j 框架入门
- 实现 AI 应用生成(2 种生成模式)
- 实现 SSE 流式输出提升用户体验
- 通过多种设计模式优化代码架构
一、需求分析
我们的需求很好理解 —— 让 AI 根据用户的描述,自动生成完整的网页应用。
本节先从最基础的需求开始,让 AI 能够生成 原生网页代码,并将代码文件保存到本地。
原生是指代码中不需要引入第三方框架(比如 Vue 和 React),简简单单、干干净净,更容易运行,实现起来更简单。
我们可以采用 2 种原生生成模式来满足不同的使用场景:
- 原生 HTML 模式:将所有代码(HTML、CSS、JS)打包在一个 HTML 文件中,适合快速原型和简单应用
- 原生多文件模式:按照标准的前端项目结构,分别生成 index.html、style.css 和 script.js 文件
二、方案设计
在正式开始编码之前,我们需要先明确整个 AI 应用生成的核心流程:
用户输入描述 → AI 大模型生成 → 提取生成内容 → 写入本地文件
这个看似简单的流程,实际上涉及不少技术细节。比如:
- 如何实现和 AI 的对话?
- 如何设计有效的提示词?
- 如何确保 AI 输出的格式符合我们的要求?
- 如何处理生成的代码并保存到合适的位置?
编写系统提示词
提示词的质量直接决定了 AI 生成结果的好坏。在这个环节,我们可以参考网上的 Prompt 编写指南 以及鱼皮 AI 超级智能体项目 中的 Prompt 指南。
有个小技巧是直接让 AI 帮我们根据需求来生成提示词,比如我向 AI 提问:
我正在做一个 AI 零代码应用生成平台,根据用户的一段描述即可生成一个完整网站。生成的网站使用 HTML + CSS + JS 实现,帮我编写一个专业的 Prompt。
参考资料:<https://help.aliyun.com/zh/model-studio/use-cases/prompt-engineering-guide>
经过多轮调试和优化,我最终确定了两套系统提示词模板:
1)生成单个 HTML 文件模式的提示词:
你是一位资深的 Web 前端开发专家,精通 HTML、CSS 和原生 JavaScript。你擅长构建响应式、美观且代码整洁的单页面网站。
你的任务是根据用户提供的网站描述,生成一个完整、独立的单页面网站。你需要一步步思考,并最终将所有代码整合到一个 HTML 文件中。
约束:
1. 技术栈: 只能使用 HTML、CSS 和原生 JavaScript。
2. 禁止外部依赖: 绝对不允许使用任何外部 CSS 框架、JS 库或字体库。所有功能必须用原生代码实现。
3. 独立文件: 必须将所有的 CSS 代码都内联在 `<head>` 标签的 `` 标签内,并将所有的 JavaScript 代码都放在 `</body>` 标签之前的 `` 标签内。最终只输出一个 `.html` 文件,不包含任何外部文件引用。
4. 响应式设计: 网站必须是响应式的,能够在桌面和移动设备上良好显示。请优先使用 Flexbox 或 Grid 进行布局。
5. 内容填充: 如果用户描述中缺少具体文本或图片,请使用有意义的占位符。例如,文本可以使用 Lorem Ipsum,图片可以使用 <https://picsum.photos> 的服务 (例如 `<img src="<https://picsum.photos/800/600>" alt="Placeholder Image">`)。
6. 代码质量: 代码必须结构清晰、有适当的注释,易于阅读和维护。
7. 交互性: 如果用户描述了交互功能 (如 Tab 切换、图片轮播、表单提交提示等),请使用原生 JavaScript 来实现。
8. 安全性: 不要包含任何服务器端代码或逻辑。所有功能都是纯客户端的。
9. 输出格式: 你的最终输出必须包含 HTML 代码块,可以在代码块之外添加解释、标题或总结性文字。格式如下:
```html
... HTML 代码 ...
这里值得一提的是 Lorem ipsum 的使用。Lorem ipsum 是印刷排版行业使用的虚拟文本,主要用于测试文章或文字在不同字型、版型下的视觉效果。在我们的场景中,当用户描述比较简略时,AI 可以用这类占位符内容来完善页面结构。
2)生成多文件模式的提示词:
你是一位资深的 Web 前端开发专家,你精通编写结构化的 HTML、清晰的 CSS 和高效的原生 JavaScript,遵循代码分离和模块化的最佳实践。
你的任务是根据用户提供的网站描述,创建构成一个完整单页网站所需的三个核心文件:HTML, CSS, 和 JavaScript。你需要在最终输出时,将这三部分代码分别放入三个独立的 Markdown 代码块中,并明确标注文件名。
约束:
1. 技术栈: 只能使用 HTML、CSS 和原生 JavaScript。
2. 文件分离:
- index.html: 只包含网页的结构和内容。它必须在 `<head>` 中通过 `` 标签引用 `style.css`,并且在 `</body>` 结束标签之前通过 `` 标签引用 `script.js`。
- style.css: 包含网站所有的样式规则。
- script.js: 包含网站所有的交互逻辑。
3. 禁止外部依赖: 绝对不允许使用任何外部 CSS 框架、JS 库或字体库。所有功能必须用原生代码实现。
4. 响应式设计: 网站必须是响应式的,能够在桌面和移动设备上良好显示。请在 CSS 中使用 Flexbox 或 Grid 进行布局。
5. 内容填充: 如果用户描述中缺少具体文本或图片,请使用有意义的占位符。例如,文本可以使用 Lorem Ipsum,图片可以使用 <https://picsum.photos> 的服务 (例如 `<img src="<https://picsum.photos/800/600>" alt="Placeholder Image">`)。
6. 代码质量: 代码必须结构清晰、有适当的注释,易于阅读和维护。
7. 输出格式: 每个代码块前要注明文件名。可以在代码块之外添加解释、标题或总结性文字。格式如下:
```html
... HTML 代码 ...
```css
... CSS 代码 ...
```javascript
... JavaScript 代码 ...
为什么这个 Prompt 会很有效?大家可以关注下面几点:
- 角色扮演:让 AI 的回答更具专业性。
- 严格约束:排除了所有可能导致问题的变量(如外部库、多文件),确保了输出的稳定性和可用性。
- 清晰的输出格式要求:使得程序可以轻松集成,无需复杂的解析逻辑。
- Few-shot 示例:为 AI 提供了一个非常具体的模仿对象,大大提高了输出结果和预期结果的相似度,示例中包含了 HTML、CSS 和 JS,完整地展示了最终产物的形态。
- 细节考虑:Prompt 中考虑到了响应式设计、占位符等实际开发中常见的问题。不过这点需要发挥一下自己的网站开发经验。
在编写 Prompt 的过程中,我们可以利用一些 大模型开发平台 进行测试,直到满足需求:
如果反复调整提示词后,还是无法生成符合预期的内容,可能就意味着大模型的能力不够了。
大模型技术选型
- *大模型的能力决定了 AI 生成结果的上限。**一般我选择大模型时,会重点关注:效果、成本、生成速度、开发成本、性能、生态。
像我平时用的比较多的是阿里的通义系列大模型,因为阿里的生态做的不错,能很轻松地在 Java 中接入。
所以在选择具体的大模型服务时,我会优先使用 阿里云百炼平台 进行效果测试,发现它的输出结构是能够满足需求的。
但是经过对比测试,我发现 DeepSeek R1 在代码生成任务上表现更出色,下面这个是我用编写好的提示词生成的网站:
除了生成效果,我们还需要考虑开发成本和功能完整性。查看 DeepSeek 的官方文档 后,我发现它与 OpenAI 的 API 完全兼容,这意味着我们可以直接使用现有的 OpenAI 客户端库来接入 DeepSeek。
在具体的模型选择上,我建议开发阶段使用 deepseek-chat,因为响应速度更快,有利于提高开发效率;如果追求更高的生成质量或需要处理更长的文档,可以使用 deepseek-reasoner:
AI 开发框架选型
AI 开发框架的作用是 简化项目中开发 AI 应用的过程。实际企业开发中,可不是调用一下 AI 的 API 接口这么简单,还有像 RAG 检索增强生成、Tools 工具调用、MCP 模型上下文协议等典型场景,这些功能如果都自己开发,那成本可就太高了。
实际开发中应该如何选择 AI 开发框架呢?
目前主流的 Java AI 开发框架当属 Spring AI 和 LangChain4j。我其实会更喜欢 Spring AI 的开发模式,而且 Spring AI 目前支持的能力更多,还有国内 Spring AI Alibaba 的巨头加持,生态更好,遇到问题更容易解决;LangChain4j 的优势在于可以独立于 Spring 项目使用,更自由灵活一些。
不过这类框架大家重点学习一个就好了,很多概念和用法是相通的:
由于在 编程导航的上个项目 - AI 超级智能体 中,鱼皮已经带大家实战了 Spring AI 的核心特性,所以在本项目中,专门换一个新的框架 LangChain4j,带大家多学习一门新的技术,这样以后企业无论用哪个,咱都会!嘎嘎猛~
三、LangChain4j 入门
LangChain4j 是目前主流的 Java AI 开发框架。我看重它的 3 大优势:
- 声明式编程模式:通过简单的注解和接口定义,就能实现复杂的 AI 交互逻辑,这大大降低了开发门槛
- 丰富的模型支持:不仅支持 OpenAI,还兼容国内外主流的大模型服务
- 容易集成:它和 Spring Boot 的集成做得非常好,能快速整合到已有项目中
如果你对 LangChain4j 一无所知,强烈推荐先学习鱼皮之前写的 LangChain4j 保姆级项目实战教程,大概 2 小时左右就能实践一遍 LangChain4j 的关键特性,并且开发一个低配版的 AI 编程小助手。
我们需要掌握的 LangChain4j 特性如下:
- AI 对话 - ChatModel
- 多模态 - Multimodality
- 系统提示词 - SystemMessage
- AI 服务 - AI Service
- Spring Boot 项目整合
- 会话记忆 - ChatMemory
- 结构化输出
- 检索增强生成 - RAG
- 工具调用 - Tools
- 模型上下文协议 - MCP
- 护轨 - Guardrail
- 日志和可观测性
不过哪怕不看前面提到的教程,也能跟得上这个项目,而且本项目中对 LangChain4j 的应用会更加深入。建议大家在学习本项目时,结合阅读 官方文档 进行巩固。
四、实现 AI 应用生成(原生模式)
确定了技术方案后,我们开始实现。
1、大模型接入
首先需要获取 DeepSeek 的 API Key,访问 官方文档:
接下来引入必要的依赖。参考 LangChain4j 的官方文档,我们需要添加 OpenAI 大模型的依赖:
在 pom.xml 中添加依赖:
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>1.1.0</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
<version>1.1.0-beta7</version>
</dependency>
为了保护敏感信息,我们需要在 .gitignore 中添加本地配置文件 application-local.yml,忽略该文件的提交,之后就可以放心地将敏感配置都写在这个文件里了。
### CUSTOM ###
application-local.yml
创建本地配置文件 application-local.yml,填写 Chat Model 配置。此外,为了调试方便,我们开启了详细的日志记录。这里参考了 LangChain4j 的日志配置文档:
# AI
langchain4j:
open-ai:
chat-model:
base-url: <https://api.deepseek.com>
api-key: <Your API Key>
model-name: deepseek-chat
log-requests: true
log-responses: true
在主配置文件中激活本地环境:
spring:
profiles:
active: local
2、开发 AI 服务
按照 LangChain4j 推荐的 AI Service 开发模式,在 ai 包下创建服务接口:
public interface AiCodeGeneratorService {
String generateCode(String userMessage);
}
考虑到系统提示词通常比较长,将它们单独维护在资源文件中。准备了两种生成模式对应的系统提示词文件:
codegen-html-system-prompt.txt:原生 HTML 模式codegen-multi-file-system-prompt.txt:原生三件套模式
在服务接口中添加 2 个生成代码的方法,分别对应 2 种生成模式。使用 LangChain4j 的注解来指定系统提示词:
public interface AiCodeGeneratorService {
/**
* 生成 HTML 代码
*
* @param userMessage 用户消息
* @return 生成的代码结果
*/
@SystemMessage(fromResource = "prompt/codegen-html-system-prompt.txt")
String generateHtmlCode(String userMessage);
/**
* 生成多文件代码
*
* @param userMessage 用户消息
* @return 生成的代码结果
*/
@SystemMessage(fromResource = "prompt/codegen-multi-file-system-prompt.txt")
String generateMultiFileCode(String userMessage);
}
创建工厂类来初始化 AI 服务:
@Configuration
public class AiCodeGeneratorServiceFactory {
@Resource
private ChatModel chatModel;
@Bean
public AiCodeGeneratorService aiCodeGeneratorService() {
return AiServices.create(AiCodeGeneratorService.class, chatModel);
}
}
最后,编写单元测试来验证功能:
@SpringBootTest
class AiCodeGeneratorServiceTest {
@Resource
private AiCodeGeneratorService aiCodeGeneratorService;
@Test
void generateHtmlCode() {
String result = aiCodeGeneratorService.generateHtmlCode("做个程序员鱼皮的工作记录小工具");
Assertions.assertNotNull(result);
}
@Test
void generateMultiFileCode() {
String multiFileCode = aiCodeGeneratorService.generateMultiFileCode("做个程序员鱼皮的留言板");
Assertions.assertNotNull(multiFileCode);
}
}
以 Debug 方式运行测试,可以在控制台看到详细的请求和响应日志
测试结果符合预期,AI 成功生成了完整的网页代码:


3、结构化输出
虽然已经能够调用 AI 生成代码,但直接返回字符串的方式不便于后续解析代码并保存为文件。因此我们需要将 AI 的输出转换为结构化的对象,利用 LangChain4j 的结构化输出 特性可以轻松实现。
基本实现
1)在 ai.model 包下创建生成结果类,用于封装 AI 返回的内容:
@Data
public class HtmlCodeResult {
private String htmlCode;
private String description;
}
@Data
public class MultiFileCodeResult {
private String htmlCode;
private String cssCode;
private String jsCode;
private String description;
}
2)修改 AI 服务接口,让方法返回结构化对象:
/**
* 生成 HTML 代码
*
* @param userMessage 用户消息
* @return 生成的代码结果
*/
@SystemMessage(fromResource = "prompt/codegen-html-system-prompt.txt")
HtmlCodeResult generateHtmlCode(String userMessage);
/**
* 生成多文件代码
*
* @param userMessage 用户消息
* @return 生成的代码结果
*/
@SystemMessage(fromResource = "prompt/codegen-multi-file-system-prompt.txt")
MultiFileCodeResult generateMultiFileCode(String userMessage);
3)相应地修改单元测试调用方法的返回值类型:
@Test
void generateHtmlCode() {
HtmlCodeResult result = aiCodeGeneratorService.generateHtmlCode("做个程序员鱼皮的工作记录小工具");
Assertions.assertNotNull(result);
}
@Test
void generateMultiFileCode() {
MultiFileCodeResult multiFileCode = aiCodeGeneratorService.generateMultiFileCode("做个程序员鱼皮的留言板");
Assertions.assertNotNull(multiFileCode);
}
4)执行单元测试,通过日志可以看到,LangChain4j 自动在我们的提示词后面拼接了结构化输出的要求:
但是在实际测试中出现了报错,AI 好像并没有乖乖地返回我们预期的 JSON 格式!
优化技巧
经过研究和实践,我总结了 4 个关键的优化技巧,来提高结构化输出的准确度和稳定性。
1)设置 max_tokens
参考 DeepSeek 官方文档 的建议,设置一下输出长度,防止 AI 生成的 JSON 被半路截断:
注意不要超出模型的限制:
在项目配置文件中添加:
langchain4j:
open-ai:
chat-model:
max-tokens: 8192
2)JSON Schema 配置
OpenAI 相关文档 提到了 response_format_json_schema 配置,可以严格确保结构化输出生效:
但经过测试发现,DeepSeek 不支持这种配置,项目中使用会报错。
不过官方文档提到了另外一种配置,设置 response-format 参数为 json_object。
可以在项目中使用这个配置:
langchain4j:
open-ai:
chat-model:
strict-json-schema: true
response-format: json_object
3)添加字段描述
参考 LangChain4j 文档,为结果类和属性添加详细的描述信息,便于 AI 理解:
@Description("生成 HTML 代码文件的结果")
@Data
public class HtmlCodeResult {
@Description("HTML代码")
private String htmlCode;
@Description("生成代码的描述")
private String description;
}
@Description("生成多个代码文件的结果")
@Data
public class MultiFileCodeResult {
@Description("HTML代码")
private String htmlCode;
@Description("CSS代码")
private String cssCode;
@Description("JS代码")
private String jsCode;
@Description("生成代码的描述")
private String description;
}
再次运行测试,可以看到提示词中自动补充了字段描述和配置信息:
现在 AI 结构化输出的稳定性高了不少:
多文件生成也正常工作:
经过这些优化,结构化输出的准确度有了显著提升。虽然偶尔还会有不稳定的情况,但这是 DeepSeek 大模型的正常现象(DeepSeek 官方也提到了)。LangChain4j 默认会自动重试,我们也可以通过调整 max-retries 参数来控制重试次数。
4)提示词优化
最后一个技巧是在系统提示词中明确要求输出 JSON 格式,这样可以进一步提高成功率。
4、程序处理写入 - 门面模式
有了结构化的输出对象,接下来就是将生成的代码保存到本地文件系统。
1)首先定义生成类型枚举,放在 model.enums 包下,因为后续其他的 Service 应该会用到。
@Getter
public enum CodeGenTypeEnum {
HTML("原生 HTML 模式", "html"),
MULTI_FILE("原生多文件模式", "multi_file");
private final String text;
private final String value;
CodeGenTypeEnum(String text, String value) {
this.text = text;
this.value = value;
}
/**
* 根据 value 获取枚举
*
* @param value 枚举值的value
* @return 枚举值
*/
public static CodeGenTypeEnum getEnumByValue(String value) {
if (ObjUtil.isEmpty(value)) {
return null;
}
for (CodeGenTypeEnum anEnum : CodeGenTypeEnum.values()) {
if (anEnum.value.equals(value)) {
return anEnum;
}
}
return null;
}
}
2)创建文件写入工具类 CodeFileSaver,放在 core 包下,表示核心业务代码。
我选择在临时目录 tmp 下保存文件,每次生成都对应一个临时目录下的文件夹,使用 业务类型 + 雪花 ID 的命名方式来确保唯一性。
可以使用 Hutool 工具库实现雪花 ID,代码如下:
public class CodeFileSaver {
// 文件保存根目录
private static final String FILE_SAVE_ROOT_DIR = System.getProperty("user.dir") + "/tmp/code_output";
/**
* 保存 HtmlCodeResult
*/
public static File saveHtmlCodeResult(HtmlCodeResult result) {
String baseDirPath = buildUniqueDir(CodeGenTypeEnum.HTML.getValue());
writeToFile(baseDirPath, "index.html", result.getHtmlCode());
return new File(baseDirPath);
}
/**
* 保存 MultiFileCodeResult
*/
public static File saveMultiFileCodeResult(MultiFileCodeResult result) {
String baseDirPath = buildUniqueDir(CodeGenTypeEnum.MULTI_FILE.getValue());
writeToFile(baseDirPath, "index.html", result.getHtmlCode());
writeToFile(baseDirPath, "style.css", result.getCssCode());
writeToFile(baseDirPath, "script.js", result.getJsCode());
return new File(baseDirPath);
}
/**
* 构建唯一目录路径:tmp/code_output/bizType_雪花ID
*/
private static String buildUniqueDir(String bizType) {
String uniqueDirName = StrUtil.format("{}_{}", bizType, IdUtil.getSnowflakeNextIdStr());
String dirPath = FILE_SAVE_ROOT_DIR + File.separator + uniqueDirName;
FileUtil.mkdir(dirPath);
return dirPath;
}
/**
* 写入单个文件
*/
private static void writeToFile(String dirPath, String filename, String content) {
String filePath = dirPath + File.separator + filename;
FileUtil.writeString(content, filePath, StandardCharsets.UTF_8);
}
}
记得将 tmp 目录添加到 .gitignore 中,避免生成的文件被提交到代码仓库。
3)为了统一管理生成和保存的逻辑,我决定使用 门面模式 这一设计模式。
门面模式通过提供一个统一的高层接口来隐藏子系统的复杂性,让客户端只需要与这个简化的接口交互,而不用了解内部的复杂实现细节。
在 core 包下创建 AiCodeGeneratorFacade 类,代码如下:
/**
* AI 代码生成外观类,组合生成和保存功能
*/
@Service
public class AiCodeGeneratorFacade {
@Resource
private AiCodeGeneratorService aiCodeGeneratorService;
/**
* 统一入口:根据类型生成并保存代码
*
* @param userMessage 用户提示词
* @param codeGenTypeEnum 生成类型
* @return 保存的目录
*/
public File generateAndSaveCode(String userMessage, CodeGenTypeEnum codeGenTypeEnum) {
if (codeGenTypeEnum == null) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "生成类型为空");
}
return switch (codeGenTypeEnum) {
case HTML -> generateAndSaveHtmlCode(userMessage);
case MULTI_FILE -> generateAndSaveMultiFileCode(userMessage);
default -> {
String errorMessage = "不支持的生成类型:" + codeGenTypeEnum.getValue();
throw new BusinessException(ErrorCode.SYSTEM_ERROR, errorMessage);
}
};
}
/**
* 生成 HTML 模式的代码并保存
*
* @param userMessage 用户提示词
* @return 保存的目录
*/
private File generateAndSaveHtmlCode(String userMessage) {
HtmlCodeResult result = aiCodeGeneratorService.generateHtmlCode(userMessage);
return CodeFileSaver.saveHtmlCodeResult(result);
}
/**
* 生成多文件模式的代码并保存
*
* @param userMessage 用户提示词
* @return 保存的目录
*/
private File generateAndSaveMultiFileCode(String userMessage) {
MultiFileCodeResult result = aiCodeGeneratorService.generateMultiFileCode(userMessage);
return CodeFileSaver.saveMultiFileCodeResult(result);
}
}
4)编写单元测试验证整个流程:
@SpringBootTest
class AiCodeGeneratorFacadeTest {
@Resource
private AiCodeGeneratorFacade aiCodeGeneratorFacade;
@Test
void generateAndSaveCode() {
File file = aiCodeGeneratorFacade.generateAndSaveCode("任务记录网站", CodeGenTypeEnum.MULTI_FILE);
Assertions.assertNotNull(file);
}
}
测试成功,可以看到生成文件的保存路径:
双击生成文件的 index.html,即可看到生成的网站效果:
五、SSE 流式输出
到这一步,基本的代码生成功能已经可以正常工作了。但是我们发现了一个问题:结构化输出的速度比较慢,用户需要等待较长时间才能看到结果,这种体验显然不够好。
为了提升用户体验,需要引入 SSE(Server-Sent Events)流式输出,像打字机一样,AI 返回一个词,前端输出一个词。
方案选择
目前流式输出不支持结构化输出,但我们可以在流式返回的过程中 拼接 AI 的返回结果(可以实时返回给前端),等全部输出完成后,再对拼接结果进行解析和保存。这样既保证了实时性,又不影响最终的处理流程。
在实现 SSE 的技术方案上,LangChain4j 提供了两种方式:
1、LangChain4j + Reactor(推荐)
Reactor 是指响应式编程,LangChain4j 提供了响应式编程依赖包,可以直接把 AI 返回的内容封装为更通用的 Flux 响应式对象。可以把 Flux 想象成一个数据流,有了这个对象后,上游发来一块数据,下游就能处理一块数据。
)
我们可以对 Flux 对象进行下列操作:
这种方案的优点是与前端集成更方便,通过 Flux 对象可以很容易地将流式内容返回给前端。缺点是需要引入额外的依赖:
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-reactor</artifactId>
<version>1.1.0-beta7</version>
</dependency>
2、TokenStream(了解即可)
这是 LangChain4j 的原生实现方式,好处是提供了更多高级回调,比如工具调用完成回调(onToolExecuted)、工具调用内容实时响应。但缺点是使用起来相对复杂,而且要返回前端时还需要用 Flux 包装一层。
示例代码:
return Flux.create(sink -> {
StringBuilder respContent = new StringBuilder();
assistant.chat(finalUserPrompt) // 返回 tokenStream
.onPartialResponse(partialResponse -> {
log.info("partialResponse: {}", partialResponse);
sink.next(partialResponse);
})
.onCompleteResponse(completeResponse -> {
log.info("chatResponse: {}", completeResponse);
sink.complete();
})
.onToolExecuted(toolExecution -> {
log.info("tool executed successfully: {}", toolExecution);
})
.onError(sink::error)
.start();
});
我最终选择了方案 1,因为它更适合我们目前的使用场景,本项目后续会实战方案 2。
开发实现
1)首先配置流式模型:
langchain4j:
open-ai:
streaming-chat-model:
base-url: <https://api.deepseek.com>
api-key: <Your API Key>
model-name: deepseek-chat
max-tokens: 8192
log-requests: true
log-responses: true
2)在创建 AI Service 的工厂类中注入流式模型:
@Configuration
public class AiCodeGeneratorServiceFactory {
@Resource
private ChatModel chatModel;
@Resource
private StreamingChatModel streamingChatModel;
@Bean
public AiCodeGeneratorService aiCodeGeneratorService() {
return AiServices.builder(AiCodeGeneratorService.class)
.chatModel(chatModel)
.streamingChatModel(streamingChatModel)
.build();
}
}
3)在 AI Service 中新增流式方法,跟之前方法的区别在于返回值改为了 Flux 对象:
/**
* 生成 HTML 代码(流式)
*
* @param userMessage 用户消息
* @return 生成的代码结果
*/
@SystemMessage(fromResource = "prompt/codegen-html-system-prompt.txt")
Flux<String> generateHtmlCodeStream(String userMessage);
/**
* 生成多文件代码(流式)
*
* @param userMessage 用户消息
* @return 生成的代码结果
*/
@SystemMessage(fromResource = "prompt/codegen-multi-file-system-prompt.txt")
Flux<String> generateMultiFileCodeStream(String userMessage);
4)编写解析逻辑。
由于流式输出返回的是字符串片段,我们需要在 AI 全部返回完成后进行解析。
由于代码解析逻辑相对复杂,单独在 core 包下创建代码解析器 CodeParser。核心逻辑是通过正则表达式从完整字符串中提取到对应的代码块,并返回结构化输出对象,这样可以复用之前的文件保存器。
/**
* 代码解析器
* 提供静态方法解析不同类型的代码内容
*
* @author yupi
*/
public class CodeParser {
private static final Pattern HTML_CODE_PATTERN = Pattern.compile("```html\\\\s*\\\\n([\\\\s\\\\S]*?)```", Pattern.CASE_INSENSITIVE);
private static final Pattern CSS_CODE_PATTERN = Pattern.compile("```css\\\\s*\\\\n([\\\\s\\\\S]*?)```", Pattern.CASE_INSENSITIVE);
private static final Pattern JS_CODE_PATTERN = Pattern.compile("```(?:js|javascript)\\\\s*\\\\n([\\\\s\\\\S]*?)```", Pattern.CASE_INSENSITIVE);
/**
* 解析 HTML 单文件代码
*/
public static HtmlCodeResult parseHtmlCode(String codeContent) {
HtmlCodeResult result = new HtmlCodeResult();
// 提取 HTML 代码
String htmlCode = extractHtmlCode(codeContent);
if (htmlCode != null && !htmlCode.trim().isEmpty()) {
result.setHtmlCode(htmlCode.trim());
} else {
// 如果没有找到代码块,将整个内容作为HTML
result.setHtmlCode(codeContent.trim());
}
return result;
}
/**
* 解析多文件代码(HTML + CSS + JS)
*/
public static MultiFileCodeResult parseMultiFileCode(String codeContent) {
MultiFileCodeResult result = new MultiFileCodeResult();
// 提取各类代码
String htmlCode = extractCodeByPattern(codeContent, HTML_CODE_PATTERN);
String cssCode = extractCodeByPattern(codeContent, CSS_CODE_PATTERN);
String jsCode = extractCodeByPattern(codeContent, JS_CODE_PATTERN);
// 设置HTML代码
if (htmlCode != null && !htmlCode.trim().isEmpty()) {
result.setHtmlCode(htmlCode.trim());
}
// 设置CSS代码
if (cssCode != null && !cssCode.trim().isEmpty()) {
result.setCssCode(cssCode.trim());
}
// 设置JS代码
if (jsCode != null && !jsCode.trim().isEmpty()) {
result.setJsCode(jsCode.trim());
}
return result;
}
/**
* 提取HTML代码内容
*
* @param content 原始内容
* @return HTML代码
*/
private static String extractHtmlCode(String content) {
Matcher matcher = HTML_CODE_PATTERN.matcher(content);
if (matcher.find()) {
return matcher.group(1);
}
return null;
}
/**
* 根据正则模式提取代码
*
* @param content 原始内容
* @param pattern 正则模式
* @return 提取的代码
*/
private static String extractCodeByPattern(String content, Pattern pattern) {
Matcher matcher = pattern.matcher(content);
if (matcher.find()) {
return matcher.group(1);
}
return null;
}
}
建议编写一个单元测试类,来验证解析器的功能:
class CodeParserTest {
@Test
void parseHtmlCode() {
String codeContent = """
随便写一段描述:
html 格式
<!DOCTYPE html>
<html>
<head>
<title>测试页面</title>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
随便写一段描述
""";
HtmlCodeResult result = CodeParser.parseHtmlCode(codeContent);
assertNotNull(result);
assertNotNull(result.getHtmlCode());
}
@Test
void parseMultiFileCode() {
String codeContent = """
创建一个完整的网页:
html 格式
<!DOCTYPE html>
<html>
<head>
<title>多文件示例</title>
</head>
<body>
<h1>欢迎使用</h1>
</body>
</html>
css 格式
h1 {
color: blue;
text-align: center;
}
```
```js
console.log('页面加载完成');
文件创建完成!
""";
MultiFileCodeResult result = CodeParser.parseMultiFileCode(codeContent);
assertNotNull(result);
assertNotNull(result.getHtmlCode());
assertNotNull(result.getCssCode());
assertNotNull(result.getJsCode());
}
}
执行单元测试,确保解析正确:
目前我们没有支持解析描述字段,感兴趣的同学可以尝试把描述也解析了(不过我觉得意义不大)。
5)在 AiCodeGeneratorFacade 中添加流式调用 AI 的方法。
针对每种生成模式,分别提供一个 “生成代码并保存” 的方法,核心逻辑都是:拼接 AI 实时响应的字符串,并在 流式返回完成后解析字符串并保存代码文件。
代码如下:
/**
* 生成 HTML 模式的代码并保存(流式)
*
* @param userMessage 用户提示词
* @return 保存的目录
*/
private Flux<String> generateAndSaveHtmlCodeStream(String userMessage) {
Flux<String> result = aiCodeGeneratorService.generateHtmlCodeStream(userMessage);
// 当流式返回生成代码完成后,再保存代码
StringBuilder codeBuilder = new StringBuilder();
return result
.doOnNext(chunk -> {
// 实时收集代码片段
codeBuilder.append(chunk);
})
.doOnComplete(() -> {
// 流式返回完成后保存代码
try {
String completeHtmlCode = codeBuilder.toString();
HtmlCodeResult htmlCodeResult = CodeParser.parseHtmlCode(completeHtmlCode);
// 保存代码到文件
File savedDir = CodeFileSaver.saveHtmlCodeResult(htmlCodeResult);
log.info("保存成功,路径为:" + savedDir.getAbsolutePath());
} catch (Exception e) {
log.error("保存失败: {}", e.getMessage());
}
});
}
/**
* 生成多文件模式的代码并保存(流式)
*
* @param userMessage 用户提示词
* @return 保存的目录
*/
private Flux<String> generateAndSaveMultiFileCodeStream(String userMessage) {
Flux<String> result = aiCodeGeneratorService.generateMultiFileCodeStream(userMessage);
// 当流式返回生成代码完成后,再保存代码
StringBuilder codeBuilder = new StringBuilder();
return result
.doOnNext(chunk -> {
// 实时收集代码片段
codeBuilder.append(chunk);
})
.doOnComplete(() -> {
// 流式返回完成后保存代码
try {
String completeMultiFileCode = codeBuilder.toString();
MultiFileCodeResult multiFileResult = CodeParser.parseMultiFileCode(completeMultiFileCode);
// 保存代码到文件
File savedDir = CodeFileSaver.saveMultiFileCodeResult(multiFileResult);
log.info("保存成功,路径为:" + savedDir.getAbsolutePath());
} catch (Exception e) {
log.error("保存失败: {}", e.getMessage());
}
});
}
6)在 AiCodeGeneratorFacade 中编写统一入口,根据生成模式枚举选择对应的流式方法:
/**
* 统一入口:根据类型生成并保存代码(流式)
*
* @param userMessage 用户提示词
* @param codeGenTypeEnum 生成类型
*/
public Flux<String> generateAndSaveCodeStream(String userMessage, CodeGenTypeEnum codeGenTypeEnum) {
if (codeGenTypeEnum == null) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "生成类型为空");
}
return switch (codeGenTypeEnum) {
case HTML -> generateAndSaveHtmlCodeStream(userMessage);
case MULTI_FILE -> generateAndSaveMultiFileCodeStream(userMessage);
default -> {
String errorMessage = "不支持的生成类型:" + codeGenTypeEnum.getValue();
throw new BusinessException(ErrorCode.SYSTEM_ERROR, errorMessage);
}
};
}
7)编写单元测试验证流式功能:
@Test
void generateAndSaveCodeStream() {
Flux<String> codeStream = aiCodeGeneratorFacade.generateAndSaveCodeStream("任务记录网站", CodeGenTypeEnum.MULTI_FILE);
// 阻塞等待所有数据收集完成
List<String> result = codeStream.collectList().block();
// 验证结果
Assertions.assertNotNull(result);
String completeContent = String.join("", result);
Assertions.assertNotNull(completeContent);
}
测试过程中多多 Debug,可以看到流式响应的效果:
获取到完整返回的字符串:
最终成功解析并生成文件:
查看生成的网站效果:
六、代码优化 - 极致的抽象
在开发过程中,我们会发现大量的重复代码。作为一个有追求的程序员,我们可以通过几种设计模式来优化代码结构。
优化方案
经过仔细分析,我决定使用以下优化策略:
- 解析器部分:使用策略模式,不同类型的解析策略独立维护(难点是不同解析策略的返回值不同)
- 文件保存部分:使用模板方法模式,统一保存流程(难点是不同保存方式的方法参数不同)
- SSE 流式处理:抽象出通用的流式处理逻辑(目前每种生成模式都写了一套处理代码)
策略模式
策略模式定义了一系列算法,将每个算法封装起来,并让它们可以相互替换,使得算法的变化不会影响使用算法的代码,让项目更好维护和扩展。
模板方法模式
模板方法模式在抽象父类中定义了操作的标准流程,将一些具体实现步骤交给子类,使得子类可以在不改变流程的情况下重新定义某些特定步骤。
执行器模式
正常情况下,可以通过工厂模式来创建不同的策略或模板方法,但由于每种生成模式的参数和返回值不同(HtmlCodeResult 和 MultiFileCodeResult),很难对通过工厂模式创建出来的对象进行统一的调用。
public HtmlCodeResult parseCode(String codeContent) {}
public MultiFileCodeResult parseCode(String codeContent) {}
void saveFiles(HtmlCodeResult result, String baseDirPath) {}
void saveFiles(MultiFileCodeResult result, String baseDirPath) {}
对于方法参数不同的策略模式和模板方法模式,建议使用执行器模式(Executor)。
执行器模式提供统一的执行入口来协调不同策略和模板的调用,特别适合处理参数类型不同但业务逻辑相似的场景,避免了工厂模式在处理不同参数类型时的局限性。
混合模式
最终,我们预期的代码架构是一种 混合模式:
- 执行器模式:提供统一的执行入口,根据生成类型执行不同的操作
- 策略模式:每种模式对应的解析方法单独作为一个类来维护
- 模板方法模式:抽象模板类定义了通用的文件保存流程,子类可以有自己的实现(比如多文件生成模式需要保存 3 个文件,而原生 HTML 模式只需要保存 1 个文件)
开发实现
1、优化解析器
新建 parser 包:
定义接口,通过泛型统一方法的返回值:
/**
* 代码解析器策略接口
*
* @author yupi
*/
public interface CodeParser<T> {
/**
* 解析代码内容
*
* @param codeContent 原始代码内容
* @return 解析后的结果对象
*/
T parseCode(String codeContent);
}
编写 HTML 单文件代码解析器:
/**
* HTML 单文件代码解析器
*
* @author yupi
*/
public class HtmlCodeParser implements CodeParser<HtmlCodeResult> {
private static final Pattern HTML_CODE_PATTERN = Pattern.compile("```html\\\\s*\\\\n([\\\\s\\\\S]*?)```", Pattern.CASE_INSENSITIVE);
@Override
public HtmlCodeResult parseCode(String codeContent) {
HtmlCodeResult result = new HtmlCodeResult();
// 提取 HTML 代码
String htmlCode = extractHtmlCode(codeContent);
if (htmlCode != null && !htmlCode.trim().isEmpty()) {
result.setHtmlCode(htmlCode.trim());
} else {
// 如果没有找到代码块,将整个内容作为HTML
result.setHtmlCode(codeContent.trim());
}
return result;
}
/**
* 提取HTML代码内容
*
* @param content 原始内容
* @return HTML代码
*/
private String extractHtmlCode(String content) {
Matcher matcher = HTML_CODE_PATTERN.matcher(content);
if (matcher.find()) {
return matcher.group(1);
}
return null;
}
}
编写多文件代码解析器:
/**
* 多文件代码解析器(HTML + CSS + JS)
*
* @author yupi
*/
public class MultiFileCodeParser implements CodeParser<MultiFileCodeResult> {
private static final Pattern HTML_CODE_PATTERN = Pattern.compile("```html\\\\s*\\\\n([\\\\s\\\\S]*?)```", Pattern.CASE_INSENSITIVE);
private static final Pattern CSS_CODE_PATTERN = Pattern.compile("```css\\\\s*\\\\n([\\\\s\\\\S]*?)```", Pattern.CASE_INSENSITIVE);
private static final Pattern JS_CODE_PATTERN = Pattern.compile("```(?:js|javascript)\\\\s*\\\\n([\\\\s\\\\S]*?)```", Pattern.CASE_INSENSITIVE);
@Override
public MultiFileCodeResult parseCode(String codeContent) {
MultiFileCodeResult result = new MultiFileCodeResult();
// 提取各类代码
String htmlCode = extractCodeByPattern(codeContent, HTML_CODE_PATTERN);
String cssCode = extractCodeByPattern(codeContent, CSS_CODE_PATTERN);
String jsCode = extractCodeByPattern(codeContent, JS_CODE_PATTERN);
// 设置HTML代码
if (htmlCode != null && !htmlCode.trim().isEmpty()) {
result.setHtmlCode(htmlCode.trim());
}
// 设置CSS代码
if (cssCode != null && !cssCode.trim().isEmpty()) {
result.setCssCode(cssCode.trim());
}
// 设置JS代码
if (jsCode != null && !jsCode.trim().isEmpty()) {
result.setJsCode(jsCode.trim());
}
return result;
}
/**
* 根据正则模式提取代码
*
* @param content 原始内容
* @param pattern 正则模式
* @return 提取的代码
*/
private String extractCodeByPattern(String content, Pattern pattern) {
Matcher matcher = pattern.matcher(content);
if (matcher.find()) {
return matcher.group(1);
}
return null;
}
}
编写代码解析执行器,根据代码生成类型执行相应的解析逻辑:
/**
* 代码解析执行器
* 根据代码生成类型执行相应的解析逻辑
*
* @author yupi
*/
public class CodeParserExecutor {
private static final HtmlCodeParser htmlCodeParser = new HtmlCodeParser();
private static final MultiFileCodeParser multiFileCodeParser = new MultiFileCodeParser();
/**
* 执行代码解析
*
* @param codeContent 代码内容
* @param codeGenType 代码生成类型
* @return 解析结果(HtmlCodeResult 或 MultiFileCodeResult)
*/
public static Object executeParser(String codeContent, CodeGenTypeEnum codeGenType) {
return switch (codeGenType) {
case HTML -> htmlCodeParser.parseCode(codeContent);
case MULTI_FILE -> multiFileCodeParser.parseCode(codeContent);
default -> throw new BusinessException(ErrorCode.SYSTEM_ERROR, "不支持的代码生成类型: " + codeGenType);
};
}
}
2、优化文件保存器
新建 saver 包
创建文件保存模板抽象类,通过泛型统一了方法的调用参数。其中 saveCode 方法定义了保存代码的通用流程:
/**
* 抽象代码文件保存器 - 模板方法模式
*
* @author yupi
*/
public abstract class CodeFileSaverTemplate<T> {
// 文件保存根目录
protected static final String FILE_SAVE_ROOT_DIR = System.getProperty("user.dir") + "/tmp/code_output";
/**
* 模板方法:保存代码的标准流程
*
* @param result 代码结果对象
* @return 保存的目录
*/
public final File saveCode(T result) {
// 1. 验证输入
validateInput(result);
// 2. 构建唯一目录
String baseDirPath = buildUniqueDir();
// 3. 保存文件(具体实现由子类提供)
saveFiles(result, baseDirPath);
// 4. 返回目录文件对象
return new File(baseDirPath);
}
/**
* 验证输入参数(可由子类覆盖)
*
* @param result 代码结果对象
*/
protected void validateInput(T result) {
if (result == null) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "代码结果对象不能为空");
}
}
/**
* 构建唯一目录路径
*
* @return 目录路径
*/
protected final String buildUniqueDir() {
String codeType = getCodeType().getValue();
String uniqueDirName = StrUtil.format("{}_{}", codeType, IdUtil.getSnowflakeNextIdStr());
String dirPath = FILE_SAVE_ROOT_DIR + File.separator + uniqueDirName;
FileUtil.mkdir(dirPath);
return dirPath;
}
/**
* 写入单个文件的工具方法
*
* @param dirPath 目录路径
* @param filename 文件名
* @param content 文件内容
*/
protected final void writeToFile(String dirPath, String filename, String content) {
if (StrUtil.isNotBlank(content)) {
String filePath = dirPath + File.separator + filename;
FileUtil.writeString(content, filePath, StandardCharsets.UTF_8);
}
}
/**
* 获取代码类型(由子类实现)
*
* @return 代码生成类型
*/
protected abstract CodeGenTypeEnum getCodeType();
/**
* 保存文件的具体实现(由子类实现)
*
* @param result 代码结果对象
* @param baseDirPath 基础目录路径
*/
protected abstract void saveFiles(T result, String baseDirPath);
}
编写 HTML 代码文件保存器:
/**
* HTML代码文件保存器
*
* @author yupi
*/
public class HtmlCodeFileSaverTemplate extends CodeFileSaverTemplate<HtmlCodeResult> {
@Override
protected CodeGenTypeEnum getCodeType() {
return CodeGenTypeEnum.HTML;
}
@Override
protected void saveFiles(HtmlCodeResult result, String baseDirPath) {
// 保存 HTML 文件
writeToFile(baseDirPath, "index.html", result.getHtmlCode());
}
@Override
protected void validateInput(HtmlCodeResult result) {
super.validateInput(result);
// HTML 代码不能为空
if (StrUtil.isBlank(result.getHtmlCode())) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "HTML代码内容不能为空");
}
}
}
编写多文件代码保存器:
/**
* 多文件代码保存器
*
* @author yupi
*/
public class MultiFileCodeFileSaverTemplate extends CodeFileSaverTemplate<MultiFileCodeResult> {
@Override
public CodeGenTypeEnum getCodeType() {
return CodeGenTypeEnum.MULTI_FILE;
}
@Override
protected void saveFiles(MultiFileCodeResult result, String baseDirPath) {
// 保存 HTML 文件
writeToFile(baseDirPath, "index.html", result.getHtmlCode());
// 保存 CSS 文件
writeToFile(baseDirPath, "style.css", result.getCssCode());
// 保存 JavaScript 文件
writeToFile(baseDirPath, "script.js", result.getJsCode());
}
@Override
protected void validateInput(MultiFileCodeResult result) {
super.validateInput(result);
// 至少要有 HTML 代码,CSS 和 JS 可以为空
if (StrUtil.isBlank(result.getHtmlCode())) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "HTML代码内容不能为空");
}
}
}
编写代码文件保存执行器,根据代码生成类型执行相应的保存逻辑:
/**
* 代码文件保存执行器
* 根据代码生成类型执行相应的保存逻辑
*
* @author yupi
*/
public class CodeFileSaverExecutor {
private static final HtmlCodeFileSaverTemplate htmlCodeFileSaver = new HtmlCodeFileSaverTemplate();
private static final MultiFileCodeFileSaverTemplate multiFileCodeFileSaver = new MultiFileCodeFileSaverTemplate();
/**
* 执行代码保存
*
* @param codeResult 代码结果对象
* @param codeGenType 代码生成类型
* @return 保存的目录
*/
public static File executeSaver(Object codeResult, CodeGenTypeEnum codeGenType) {
return switch (codeGenType) {
case HTML -> htmlCodeFileSaver.saveCode((HtmlCodeResult) codeResult);
case MULTI_FILE -> multiFileCodeFileSaver.saveCode((MultiFileCodeResult) codeResult);
default -> throw new BusinessException(ErrorCode.SYSTEM_ERROR, "不支持的代码生成类型: " + codeGenType);
};
}
}
3、优化门面类
修改 AiCodeGeneratorFacade 类,抽象出通用的流式代码处理方法:
/**
* 通用流式代码处理方法
*
* @param codeStream 代码流
* @param codeGenType 代码生成类型
* @return 流式响应
*/
private Flux<String> processCodeStream(Flux<String> codeStream, CodeGenTypeEnum codeGenType) {
StringBuilder codeBuilder = new StringBuilder();
return codeStream.doOnNext(chunk -> {
// 实时收集代码片段
codeBuilder.append(chunk);
}).doOnComplete(() -> {
// 流式返回完成后保存代码
try {
String completeCode = codeBuilder.toString();
// 使用执行器解析代码
Object parsedResult = CodeParserExecutor.executeParser(completeCode, codeGenType);
// 使用执行器保存代码
File savedDir = CodeFileSaverExecutor.executeSaver(parsedResult, codeGenType);
log.info("保存成功,路径为:" + savedDir.getAbsolutePath());
} catch (Exception e) {
log.error("保存失败: {}", e.getMessage());
}
});
}
通过这些优化,我们 AiCodeGeneratorFacade 类中的 统一入口方法 可以变得更加简洁和优雅:
/**
* 统一入口:根据类型生成并保存代码
*
* @param userMessage 用户提示词
* @param codeGenTypeEnum 生成类型
* @return 保存的目录
*/
public File generateAndSaveCode(String userMessage, CodeGenTypeEnum codeGenTypeEnum) {
if (codeGenTypeEnum == null) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "生成类型为空");
}
return switch (codeGenTypeEnum) {
case HTML -> {
HtmlCodeResult result = aiCodeGeneratorService.generateHtmlCode(userMessage);
yield CodeFileSaverExecutor.executeSaver(result, CodeGenTypeEnum.HTML);
}
case MULTI_FILE -> {
MultiFileCodeResult result = aiCodeGeneratorService.generateMultiFileCode(userMessage);
yield CodeFileSaverExecutor.executeSaver(result, CodeGenTypeEnum.MULTI_FILE);
}
default -> {
String errorMessage = "不支持的生成类型:" + codeGenTypeEnum.getValue();
throw new BusinessException(ErrorCode.SYSTEM_ERROR, errorMessage);
}
};
}
/**
* 统一入口:根据类型生成并保存代码(流式)
*
* @param userMessage 用户提示词
* @param codeGenTypeEnum 生成类型
*/
public Flux<String> generateAndSaveCodeStream(String userMessage, CodeGenTypeEnum codeGenTypeEnum) {
if (codeGenTypeEnum == null) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "生成类型为空");
}
return switch (codeGenTypeEnum) {
case HTML -> {
Flux<String> codeStream = aiCodeGeneratorService.generateHtmlCodeStream(userMessage);
yield processCodeStream(codeStream, CodeGenTypeEnum.HTML);
}
case MULTI_FILE -> {
Flux<String> codeStream = aiCodeGeneratorService.generateMultiFileCodeStream(userMessage);
yield processCodeStream(codeStream, CodeGenTypeEnum.MULTI_FILE);
}
default -> {
String errorMessage = "不支持的生成类型:" + codeGenTypeEnum.getValue();
throw new BusinessException(ErrorCode.SYSTEM_ERROR, errorMessage);
}
};
}
通过上述种种优化,代码的可读性和可维护性都得到了显著提升。当我们需要支持新的生成类型时,只需要添加相应的枚举值和处理逻辑,而不需要修改主要的业务流程。
经过再次测试,所有功能都正常工作:
总结
通过这一期的学习,我们成功地将 AI 大模型集成到了后端项目中,并且掌握了以下几个关键技能:
- LangChain4j 框架的实战应用:包括如何配置模型、编写 AI Service、处理结构化输出等核心技能。
- 提示词工程的实践:学会了如何设计高效的系统提示词,让 AI 生成符合我们要求的代码。
- 流式处理的技术方案:通过 SSE 提升了用户体验,同时保证了后续处理的完整性。
- 代码架构的优化思路:运用多种设计模式让代码更加优雅和可维护。
目前,项目已经具备了基础的网站应用生成能力,不过只能在本地点击执行。下一期我们将实现应用模块,包括前后端开发,将 AI 能力真正提供给用户。
项目分区导航:⬅️ 05-Mynotes | 06-AI 生成应用 | ➡️ 07-AI_零代码应用生成平台源码
💬 评论